# 第十三章 后端统计分析

不管在什么年代,什么行业,对已沉淀的经营数据做统计分析都是必不可少的工作(当然有很多老板是经验主义者),以往我们是通过翻阅账本、记录单等纸质材料然后人工计算来得到一些自己想要的结果,再到后来稍微先进点使用Excel等工具,Excel这种神器的出现大大提高了生产力,但是其中还是存在着一些重复的工作,比如说每次都要先把数据写到Excel文件中,去掉一些不要的信息,套用公式等,假如有些数据是我们要时刻动态关注的,那在这种场景下使用Excel显然不能很好的满足我们的需求(这里还没考虑花式Excel的学习成本)。以上这些问题其实我们可以编写相应的程序代码,把一些常用的统计分析工作让系统来自动帮我们完成,我们只需要输入统计条件就能实时看到统计结果,如有必要,我们再选择导出为Excel文件即可。本章节就将带着大家来实现一些常用而且实用的统计分析功能模块,通过这些功能模块,我们能随时随地的第一时间掌握经营状况,做到心中有数,运筹帷幄。

# 指定时间订单数、营业额

首先我们实现一个老板最关心的统计分析,那就是跟钱相关的统计。我们要实现一个能查询出指定时间范围内已成交的订单数量和营业额,而且结果是要天来分组的,通过这个结果,我们可以很直观的看到某个时间周期里每天的经营变化情况,对日常监控和数据分析都能起到很大的作用。事不宜迟,在控制层下我们新增一个Statistics控制器类,在控制类中新增一个getOrderBaseStatistics()方法:

<?php


namespace app\api\controller\v1;

use think\facade\Request;

class Statistics
{
    /**
     * 指定时间范围统计订单基础数据
     * @param('start','开始时间','require|date')
     * @param('end','结束时间','require|date')
     */
    public function getOrderBaseStatistics()
    {
        $params = Request::get();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这里我们先添加一些基础代码,包括注解参数校验和参数获取,接着我们要通过模型方法来查询统计数据库中的信息,在模型层下的Order模型类下新增一个getOrderStatisticsByDate()方法:

<?php


namespace app\api\model;


class Order extends BaseModel
{
    public $autoWriteTimestamp = true;
    protected $hidden = ['delete_time'];
    // 告诉模型这个字段是json格式的数据
    protected $json = ['snap_address', 'snap_items'];
    // 设置JSON数据返回数组
    protected $jsonAssoc = true;

    /**分页查询订单列表*/
    public static function getOrdersPaginate($params){...}

    /**
     * 指定时间范围统计订单基础数据
     */
    public static function getOrderStatisticsByDate($params)
    {
        $query = [];
        // 查询时间范围
        $query[] = self::betweenTimeQuery('start', 'end', $params);
        // 查询status为2到4这个范围的记录
        // 2(已支付),3(已发货),4(已支付但缺货)
        $query[] = ['status', 'between', '2, 4'];

        $order = self::where($query)
            // 格式化create_time字段;做聚合查询
            ->field("FROM_UNIXTIME(create_time,'%Y-%m-%d') as date,
                    count(*) as count,sum(total_price) as total_price")
            // 查询结果按date字段分组,注意这里因为在field()中给create_time字段起了别名date,所以用date
            ->group("date")
            ->select();

        return $order;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

这里的查询与以往稍有不同,因为我们期望拿到的结果是能统计出一个时间范围内,每天的订单数量和营业额,所以这里需要利用模型的field()group()搭配使用来实现聚合查询和结果分组。

field()方法这一堆东西做了三件事,第一,标识了要返回的字段,因为订单表里有很多字段,但其实我们真正只需要订单日期和订单金额而已;第二,格式化create_time字段,因为我们数据库中存放的是时间戳,我们要按天来统计就得把时间戳格式化成类似2019-02-14这样只到天的日期格式,这里使用MySQL内置的FROM_UNIXTIME()函数来格式化时间戳。如果不格式化,你会发现除非你每天的订单都是同一时间生成的,不然没办法按天分组;第三,使用SQL函数实现聚合船,比如这里的count(*)是计算记录数量,sum()是计算total_price字段的累加值。

group()方法则是按照我们指定的字段名来进行结果分组,这里我们要按天来分组,所以传入date

field()方法介绍点击查看(opens new window) 、group()方法介绍点击查看(opens new window)

模型方法定义好之后,让我们回到控制层中调用一下:

<?php


namespace app\api\controller\v1;

use app\api\model\Order as OrderModel;
use think\facade\Request;
use app\lib\exception\analysis\AnalysisException;

class Statistics
{
    /**
     * 指定时间范围统计订单基础数据
     * @param('start','开始时间','require|date')
     * @param('end','结束时间','require|date')
     */
    public function getOrderBaseStatistics()
    {
        $params = Request::get();
        $result = OrderModel::getOrderStatisticsByDate($params);
        if ($result->isEmpty()) {
            throw new AnalysisException();
        }
        return $result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

这里我们调用了刚刚封装好的统计查询方法,在查询结果为空的时候抛出一个自定义异常信息,读者记得自行实现一下这个自定义异常信息类。接着来给这个控制器方法定义一条路由,打开route.php,在v1分组下新增一个analysis路由分组下并新增一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        ............................
        // 统计分析相关接口
        Route::group('analysis', function () {
            // 时间范围统计订单数据
            Route::get('order/base', 'api/v1.Statistics/getOrderBaseStatistics');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

路由定义完了之后打开Postman,按照路由信息新增并配置一个请求:

点击发送:

[
    {
        "date": "2019-09-02",
        "count": 7,
        "total_price": "0.17"
    },
    {
        "date": "2019-09-03",
        "count": 1,
        "total_price": "0.02"
    }
]
1
2
3
4
5
6
7
8
9
10
11
12

没有报错,这里我们订单数据就按天来分组了,每个分组里面是某一天里的订单总数和营业额,这里读者可以通过修改数据库中order表里记录的create_time字段,让日期更加分散一些来体验其效果。但是这里读者有没有发现一个问题,虽然这里当前作者的数据库订单表中,确实只有这两天是有数据的,这个查询结果是对的。但是,我们这个接口是用来提供统计结果的,也就是说,在2019-09-02至2019-09-30这个日期范围里,没有数据的日期也应该是要显示出来并且count、total_price字段是0,这样才符合正常统计结果的输出,而且这个日期格式也有点长。所以这里我们得来重构一下前面的代码,我们要实现当查询的日期范围里某个日期没有数据时,补全相应数量日期的元素并给个初始值。想法有了,如何落地实现呢?前面我们的实现是查询出了结果,然后直接返回,那么这里我们就肯定是需要在返回之前搞点事情,这里作者实现的思路如下:

  1. 封装一个函数,这个函数可以根据要查询的时间范围生成一个数组,数组里面每个元素是查询时间范围内每天的统计数据,初始count和total_price都是0。
  2. 将查询出来的统计结果按日期在第1点中的函数生成出来的数组中找到对应的元素并赋值来覆盖初始值。

思路有了,让我们先来动手实现一下这个函数,在项目根目录下的application\common.php里新增一个函数fill_date_range,函数内的实现如下:

/**
 * @param $queryStart string 查询条件中的开始时间
 * @param $queryEnd string 查询条件中的结束时间
 * @param $format string 日期输出格式
 * @param $stepType string 日期间距类型
 * @param int $step int 日期间距
 * @return array
 */
function fill_date_range($queryStart, $queryEnd, $format, $stepType, $extend = '',$step = 1)
{
    // 定义个空数组用于接收数组元素
    $range = [];
    // 将查询条件格式化为时间戳方便后续使用
    // 区间开始日期
    $rangeStart = strtotime($queryStart);
    // 区间结束日期
    $rangeEnd = strtotime($queryEnd);
    // 循环生成数组元素
    while ($rangeStart <= $rangeEnd) {
        // 利用PHP内置函数date()按$format参数格式化时间戳
        $formattedDate = date($format, $rangeStart);
        // 初始化数据赋值,每个一日期就是一个数组元素
        $item = [
            'date' => $formattedDate,
            'count' => 0,
        ];
        // 如果存在扩展字段,给数组追加一个元素
        if ($extend) $item[$extend] = 0;
        // 将元素追加到数组中
        array_push($range, $item);
        // 利用PHP内置函数strtotime()拿到指向下一个日期的时间戳
        $rangeStart = strtotime("+{$step} {$stepType}", $rangeStart);
    }
    // 返回包含查询条件开始到结束日期之前的所有日期元素,包含初始化数据
    return $range;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

common.php 里定义的函数会在框架启动时被加载,然后就可以在任意地方直接调用。TP5框架的助手函数实现原理也是利用这种加载机制。

这里我们定义了fill_date_range()函数,函数接收六个参数,查询的开始、结束时间、日期格式、日期间距的类型、日期间距的数值、扩展字段。$format参数主要是用来控制在生成数组元素里的date字段的值时,这个日期要以什么格式输出,为什么这么要有这个设定呢?因为我们在做统计的时候,可能是按年、月、日、时、分、秒来统计,那么我们这个补全日期的方法就要考虑下兼容各种格式输出的问题。$step$stepType参数主要作用于strtotime()PHP内置函数,通过给strtotime()函数的第一个参数传入字符串可以控制要拿到距离第二个参数多长时间的字符串,比如传入+1 day就是第二个参数时间戳基础上加上一天,另外还有+1 year+1 month等等,具体读者可以自行查阅PHP手册。默认的$step我们给了1,因为常见的统计方式是按每一年或者每个月、每一天,但是不排除会有一些特别的统计维度,比如每5天,每3天这样,所以我们稍微考虑得长远些,虽然目前我们暂时没有这个业务需求,这里顺手为日后扩展做了点准备。$extend参数用于扩展初始化数组中元素的字段内容,默认情况下每个元素只有datecount字段,但有些场景下,比如说我们的订单统计,还需要展示订单总金额,不同场景下字段名也是不确定的,所以这里我们通过一个变量来控制是否追加扩展字段并同样提供初始值。函数定义好之后,我们先不着急作用于业务模块中,我们先来测试一下这个函数能不能达到我们预期的效果,在控制层方法下,我们来写段测试代码:

<?php


namespace app\api\controller\v1;

use app\api\model\Order as OrderModel;
use think\facade\Request;
use app\lib\exception\analysis\AnalysisException;

class Statistics
{
    /**
     * 指定时间范围统计订单基础数据
     * @param('start','开始时间','require|date')
     * @param('end','结束时间','require|date')
     */
    public function getOrderBaseStatistics()
    {
        $params = Request::get();
        $result = fill_date_range($params['start'], $params['end'], 'd', 'day','total_price');
        // $result = OrderModel::getOrderBaseStatistics($params);
        // if ($result->isEmpty()) {
        //     throw new AnalysisException();
        // }
        return $result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

接着回到Postman中,再次发送请求:


// http://localhost:8000/v1/analysis/order/base?start=2019-09-02&end=2019-09-05

[
    {
        "date": "02",
        "count": 0,
        "total_price": 0
    },
    {
        "date": "03",
        "count": 0,
        "total_price": 0
    },
    {
        "date": "04",
        "count": 0,
        "total_price": 0
    },
    {
        "date": "05",
        "count": 0,
        "total_price": 0
    }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

可以看到这里返回了从2019-09-022019-09-05这个时间范围的所有日期,初始化数据也有了,这个结果符合我们的预期,接下来就要来把这个函数运用到我们的业务逻辑中去了。我们先回顾下我们之前的实现思路:

  1. 封装一个函数,这个函数可以根据要查询的时间范围生成一个数组,数组里面每个元素是查询时间范围内每天的统计数据,初始count和total_price都是0。
  2. 将查询出来的统计结果按日期在第1点中的函数生成出来的数组中找到对应的元素并赋值来覆盖初始值。

这里第1点的函数我们已经实现,接下来是实现第2点的内容了,这里粗看就知道需要一点代码量,所以我们把第2点的具体实现封装到服务层下,在服务层下我们创建一个Statistics服务类并新增一个getOrderStatisticsByDate()方法:

<?php


namespace app\api\service;


use app\api\model\Order as OrderModel;

class Statistics
{
    /**
     * @param $params
     */
    public static function getOrderStatisticsByDate($params)
    {
        // 1.根据日期间距类型返回不同应用的日期格式参数
        $format = self::handleType($params['type']);
        // 2.查询出指定时间范围内的订单统计数据
        $statisticRes = OrderModel::getOrderStatisticsByDate($params, $format['mysql']);
        // 3.生成包含指定时间范围内所有日期的初始化数组
        $range = fill_date_range($params['start'], $params['end'], $format['php'], $params['type']);
        
    }

    /**
     * 根据日期间距类型返回不同应用的日期格式化参数
     * @param $type
     * @return array
     */
    protected static function handleType($type)
    {
        $map = [
            'year' => [
                'php' => 'Y',
                'mysql' => '%Y'
            ],
            'month' => [
                'php' => 'm',
                'mysql' => '%m'
            ],
            'day' => [
                'php' => 'd',
                'mysql' => '%d'
            ],
            'hour' => [
                'php' => 'H',
                'mysql' => '%H'
            ],
            'minute' => [
                'php' => 'i',
                'mysql' => '%i'
            ],
        ];
        return $map[$type];
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

要实现思路中第2点的功能,那么前提肯定是要先得到数据库中统计的结果以及fill_date_range函数的执行结果,这两个结果的获取基本是复用之前实现的代码,但在调用上存在一些问题,Order模型中的getOrderStatisticsByDate()方法实现会用到MySQL内置的时间格式化函数,而fill_date_range也会有格式化日期的操作,利用的是PHP的内置函数,PHP和MySQL日期格式化函数接收的参数形式是不一样的,MySQL的格式化参数多了个%字符串,如果每次我们在实现类似需求的时候,都要手动去传递格式字符串,第一是麻烦,第二是很容易出错,最理想的状态是调用者只需要提供日期类型即可,所以这里我们封装了一个handleType()方法,这个方法接收一个$type参数,代表日期间距类型,方法内部根据日期类型返回不同应用的日期格式化参数格式,如果未来我们有其他类型应用,也可以很方便的在对应日期类型下追加元素,如果哪天我们想调整输出格式,也不用去找每个调用的地方修改代码,只需要调整这个方法里数组元素的值即可。

有了handleType()方法之后,我们原来Order模型类下的getOrderStatisticsByDate()方法也需要略微调整,需要接收多一个$format参数:

<?php


namespace app\api\model;


class Order extends BaseModel
{
    ........................................
    ........................................
    ........................................
    /**
     * 指定时间范围统计订单基础数据
     */
    public static function getOrderStatisticsByDate($params,$format)
    {
        $query = [];
        // 查询时间范围
        $query[] = self::betweenTimeQuery('start', 'end', $params);
        // 查询status为2到4这个范围的记录
        // 2(已支付),3(已发货),4(已支付但缺货)
        $query[] = ['status', 'between', '2, 4'];

        $order = self::where($query)
            // 格式化create_time字段;做聚合查询
            ->field("FROM_UNIXTIME(create_time,'{$format}') as date,
                    count(*) as count,sum(total_price) as total_price")
            // 查询结果按date字段分组,注意这里因为在field()中给create_time字段起了别名date,所以用date
            ->group("date")
            ->select();

        return $order;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

这里我们在使用FROM_UNIXTIME()函数时日期的格式化参数不再是硬编码,而是由外部传入一个$format变量来决定。调整完毕之后我们回到服务层下继续实现我们的代码逻辑:

<?php


namespace app\api\service;


use app\api\model\Order as OrderModel;

class Statistics
{
    /**
     * @param $params
     */
    public static function getOrderStatisticsByDate($params)
    {
        // 1.根据日期间距类型返回不同应用的日期格式参数
        $format = self::handleType($params['type']);
        // 2.查询出指定时间范围内的订单统计数据
        $statisticRes = OrderModel::getOrderStatisticsByDate($params, $format['mysql']);
        // 3.生成包含指定时间范围内所有日期的初始化数组
        $range = fill_date_range($params['start'], $params['end'], $format['php'], $params['type']);
        // 4.如果指定时间范围内的没有订单数据,直接返回初始化数组
        if ($statisticRes->isEmpty()) return $range;
        // 5.利用内置函数array_column()得到由date字段组成的数组,用于方便后续使用
        // 函数返回的数组元素顺序和原数组一致(重点)
        $rangeColumn = array_column($range, 'date');
        // 6.模型方法返回的结果是一个数据集,需要先把结果集转换成数组
        $statisticRes = $statisticRes->toArray();
        // 7.利用内置函数array_walk()给$statisticRes数组的每个元素作用一个函数
        array_walk($statisticRes, function ($item) use (&$range, $rangeColumn) {
            // 8.找出在$rangeColumn中元素值等于$statisticRes元素日期的元素,返回这个元素的key
            $key = array_search($item['date'], $rangeColumn);
            // 9.对$range指定的$key元素重新赋值,覆盖初始化数据
            $range[$key] = $item;
        });
        return $range;
    }

    /**根据日期间距类型返回不同应用的日期格式化参数*/
    protected static function handleType($type){...}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

从方法内第5个步骤开始就是在做思路2的具体实现了,这里我们利用了PHP的三个内置数组函数array_columnarray_walkarray_search来实现我们的需求,这里初看会觉得有点复杂,但是如果不使用PHP内置函数,我们这里的代码就会充斥着嵌套的foreach循环和if判断,虽然同样可以实现,但是可读性会很差。通过利用PHP的内置数组函数特点,我们可以消除那些foreach和if语句,这里如果要问为什么知道可以这么写,答案就是翻PHP的手册。数组在PHP语言里是一个很重要且强大的数据类型,PHP为数组提供了几十种操作方法,通过一个或者组合多个不同的操作方法可以用更少的代码实现很多原来需要一坨代码才能实现的功能,作者也是思考和反复尝试了几次最终决定使用这三个内置函数来组合实现。

更多array_columnarray_walkarray_search的详细介绍读者请自行查阅资料了解,最好是顺便看看关于数组的其他内置函数,留个印象即可。

服务层这里的方法实现完毕之后,让我们回到控制层,修改下控制器方法:

<?php


namespace app\api\controller\v1;

use app\api\service\Statistics as StatisticsService;
use think\facade\Request;
use app\lib\exception\analysis\AnalysisException;

class Statistics
{
    /**
     * 指定时间范围统计订单基础数据
     * @param('start','开始时间','require|date')
     * @param('end','结束时间','require|date')
     * @param('type','日期间距类型','require')
     */
    public function getOrderBaseStatistics()
    {
        $params = Request::get();
        $result = StatisticsService::getOrderStatisticsByDate($params);
        return $result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

这里的参数校验最好是实现一个自定义验证器来验证,控制type的传入内容,当校验不通过时提示支持的日期间距类型

这里控制层里我们就不是直接调用模型方法了,而是调用服务层下封装的方法,接着回到Postman中,这里我们需要新增一个调用参数type:

点击发送:

[
    {
        "date": "02",
        "count": 7,
        "total_price": "0.17"
    },
    {
        "date": "03",
        "count": 1,
        "total_price": "0.02"
    },
    {
        "date": "04",
        "count": 0,
        "total_price": 0
    },
    {
        "date": "05",
        "count": 0,
        "total_price": 0
    }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

没有报错,现在这个返回格式和内容就更符合前端展示的要求了。读者可以自行测试其他日期间距类型下的返回结果。

# 指定时间新增会员数

在前面处理完订单数据的统计之后,我们来关心另一个重要的指标即新增会员数。通常在一个互联网产品上线的初期,每日新增的会员数都是团队和老板很关心的一个数据,一些公司还会专门设立负责“用户增长”的岗位。新增会员数的统计通常用于检验拉新活动的有效性或者监控线上产品的会员发展趋势,这也是一个很实用的统计功能。有了前面小节订单统计的代码铺垫,要实现这个功能也是非常简单的,还是在控制层下的Statistics类中,新增一个getUserBaseStatistics()方法:

<?php


namespace app\api\controller\v1;

use app\api\service\Statistics as StatisticsService;
use app\lib\exception\analysis\AnalysisException;
use think\facade\Request;

class Statistics
{
    /**指定时间范围统计订单基础数据*/
    public function getOrderBaseStatistics(){...}

    /**
     * 获取会员数据基础统计
     * @param('start','开始时间','require|date')
     * @param('end','结束时间','require|date')
     * @return array
     */
    public function getUserBaseStatistics()
    {
        $params = Request::get();
        $result = StatisticsService::getUserStatisticsByDate($params);
        return $result;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

这里我们同样是调用服务层下的方法,这里方法还没创建,让我们到服务层下创建一下,在服务层下的Statistics服务类中新增一个getUserStatisticsByDate()方法:

<?php


namespace app\api\service;


use app\api\model\Order as OrderModel;

class Statistics
{
    /**
     * @param $params
     */
    public static function getOrderStatisticsByDate($params){...}

    public static function getUserStatisticsByDate($params)
    {
        // 1.根据日期类型返回不同应用的日期格式参数
        $format = self::handleType($params['type']);
        // 2.查询出指定时间范围内的新增会员统计数据
        $statisticRes = UserModel::getUserStatisticsByDate($params, $format['mysql']);
        // 3.生成包含指定时间范围内所有日期的初始化数组
        $range = fill_date_range($params['start'], $params['end'], $format['php'], $params['type']);
    }

    /**根据日期间距类型返回不同应用的日期格式化参数*/
    protected static function handleType($type){...}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

按照前面的套路,这里我们需要先查询出按日期分组的统计结果,然后利用fill_date_range()函数来实现日期填充,这里我们在模型层下的User模型中实现一个模型方法getUserStatisticsByDate()实现查询:

<?php


namespace app\api\model;

class User extends BaseModel
{
    public $autoWriteTimestamp = true;
    protected $hidden = ['delete_time', 'update_time'];

    public static function getUsersPaginate($params){...}

    public static function getUserStatisticsByDate($params,$format)
    {
        $query = [];
        $query[] = self::betweenTimeQuery('start', 'end', $params);

        $user = self::where($query)
            ->field("FROM_UNIXTIME(create_time,'{$format}') as date,
        count(*) as count")
            ->group("date")
            ->select();

        return $user;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

模型方法中的实现基本与前面我们实现订单查询时大同小异只是略作了一些修改,接着让我们回到服务层下继续实现代码:

<?php


namespace app\api\service;


use app\api\model\Order as OrderModel;

class Statistics
{
    /**
     * @param $params
     */
    public static function getOrderStatisticsByDate($params){...}

    public static function getUserStatisticsByDate($params)
    {
        // 1.根据日期类型返回不同应用的日期格式参数
        $format = self::handleType($params['type']);
        // 2.查询出指定时间范围内的新增会员统计数据
        $statisticRes = UserModel::getUserStatisticsByDate($params, $format['mysql']);
        // 3.生成包含指定时间范围内所有日期的初始化数组
        $range = fill_date_range($params['start'], $params['end'], $format['php'], $params['type']);
        // 4.如果指定时间范围内的没有会员数据,直接返回初始化数组
        if ($statisticRes->isEmpty()) return $range;
        // 5.利用内置函数array_column()得到由date字段组成的数组,用于方便后续使用
        // 函数返回的数组元素顺序和原数组一致(重点)
        $rangeColumn = array_column($range, 'date');
        // 6.把结果集转换成数组
        $statisticRes = $statisticRes->toArray();
        // 7.利用内置函数array_walk()给$statisticRes数组的每个元素作用函数
        array_walk($statisticRes, function ($item) use (&$range, $rangeColumn) {
            // 7.找出在$rangeColumn中元素值等于$statisticRes元素日期的元素,返回这个元素的key
            $key = array_search($item['date'], $rangeColumn);
            // 9.对$range指定的$key元素重新赋值,覆盖初始化数据
            $range[$key] = $item;
        });
        return $range;
    }

    /**根据日期间距类型返回不同应用的日期格式化参数*/
    protected static function handleType($type){...}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

按照前面的思路,从第4步开始,后面的代码都可以直接复制粘贴了,因为都是一模一样的逻辑,效果是没毛病的,但是,我们做了一件很不好的事,复制粘贴意味着我们存在了很多重复代码,既然这里这一大段代码是可以完全复用的,那为什么不封装一下呢?让我们来动手解决下,在当前 Statistics服务类中,我们来定义一个handleReturn()方法:

<?php


namespace app\api\service;


use app\api\model\Order as OrderModel;

class Statistics
{
    /**
     * @param $params
     */
    public static function getOrderStatisticsByDate($params){...}

    public static function getUserStatisticsByDate($params){...}

    /**
     * 查询结果处理方法
     * @param $statisticRes array 查询结果
     * @param $range array 按日期初始化的数组
     * @return array 处理后的统计结果
     */
    protected static function handleReturn($statisticRes, $range)
    {
        // 1.如果指定时间范围内的没有统计结果,直接返回初始化数组
        if ($statisticRes->isEmpty()) return $range;
        // 2.利用内置函数array_column()得到由date字段组成的数组,用于方便后续使用
        // 函数返回的数组元素顺序和原数组一致(重点)
        $rangeColumn = array_column($range, 'date');
        // 3.把结果集转换成数组
        $statisticRes = $statisticRes->toArray();
        // 4.利用内置函数array_walk()给$statisticRes数组的每个元素作用函数
        array_walk($statisticRes, function ($item) use (&$range, $rangeColumn) {
            // 5.找出在$rangeColumn中元素值等于$statisticRes元素日期的元素,返回这个元素的key
            $key = array_search($item['date'], $rangeColumn);
            // 6.对$range指定的$key元素重新赋值,覆盖初始化数据
            $range[$key] = $item;
        });
        return $range;
    }

    /**根据日期间距类型返回不同应用的日期格式化参数*/
    protected static function handleType($type){...}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

这里我们把那一坨重复的代码片段直接放到handleReturn()方法中,方法封装好之后,我们就可以用来优化下我们原来的代码了:

<?php


namespace app\api\service;


use app\api\model\Order as OrderModel;

class Statistics
{
    /**
     * @param $params
     */
    public static function getOrderStatisticsByDate($params){...}

    public static function getUserStatisticsByDate($params)
    {
        // 1.根据日期类型返回不同应用的日期格式参数
        $format = self::handleType($params['type']);
        // 2.查询出指定时间范围内的新增会员统计数据
        $statisticRes = UserModel::getUserStatisticsByDate($params, $format['mysql']);
        // 3.生成包含指定时间范围内所有日期的初始化数组
        $range = fill_date_range($params['start'], $params['end'], $format['php'], $params['type']);
        // 4.利用封装的方法处理查询结果
        $result = self::handleReturn($statisticRes, $range);
        return $result;
        // // 4.如果指定时间范围内的没有会员数据,直接返回初始化数组
        // if ($statisticRes->isEmpty()) return $range;
        // // 5.利用内置函数array_column()得到由date字段组成的数组,用于方便后续使用
        // // 函数返回的数组元素顺序和原数组一致(重点)
        // $rangeColumn = array_column($range, 'date');
        // // 6.把结果集转换成数组
        // $statisticRes = $statisticRes->toArray();
        // // 7.利用内置函数array_walk()给$statisticRes数组的每个元素作用函数
        // array_walk($statisticRes, function ($item) use (&$range, $rangeColumn) {
        //     // 7.找出在$rangeColumn中元素值等于$statisticRes元素日期的元素,返回这个元素的key
        //     $key = array_search($item['date'], $rangeColumn);
        //     // 9.对$range指定的$key元素重新赋值,覆盖初始化数据
        //     $range[$key] = $item;
        // });
        // return $range;
    }

    /**查询结果处理方法*/
    protected static function handleReturn($statisticRes, $range){...}
    /**根据日期间距类型返回不同应用的日期格式化参数*/
    protected static function handleType($type){...}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

我们可以把原来第4步开始的代码全部注释掉了,调用一下我们封装的handleReturn()方法,世界一下子清净了,然后我们连上面已经实现了的getOrderStatisticsByDate()方法也一并优化一下:

<?php


namespace app\api\service;


use app\api\model\Order as OrderModel;

class Statistics
{
    /**
     * @param $params
     */
    public static function getOrderStatisticsByDate($params)
    {
        // 1.根据日期类型返回不同应用的日期格式参数
        $format = self::handleType($params['type']);
        // 2.查询出指定时间范围内的订单统计数据
        $statisticRes = OrderModel::getOrderStatisticsByDate($params, $format['mysql']);
        // 3.生成包含指定时间范围内所有日期的初始化数组
        $range = fill_date_range($params['start'], $params['end'], $format['php'], $params['type'], 'total_price');
        $result = self::handleReturn($statisticRes, $range);
        return $result;
    }

    public static function getUserStatisticsByDate($params)
    {
        // 1.根据日期类型返回不同应用的日期格式参数
        $format = self::handleType($params['type']);
        // 2.查询出指定时间范围内的新增会员统计数据
        $statisticRes = UserModel::getUserStatisticsByDate($params, $format['mysql']);
        // 3.生成包含指定时间范围内所有日期的初始化数组
        $range = fill_date_range($params['start'], $params['end'], $format['php'], $params['type']);
        // 4.利用封装的方法处理查询结果
        $result = self::handleReturn($statisticRes, $range);
        return $result;
    }

    /**查询结果处理方法*/
    protected static function handleReturn($statisticRes, $range){...}
    /**根据日期间距类型返回不同应用的日期格式化参数*/
    protected static function handleType($type){...}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

是不是感觉一下子舒服了许多?后面类似的统计接口随着业务需求的增加还会越来越多,通过封装的handleReturn()方法,可以为你节省大量的代码,但这个方法的作用不止在于减少我们的重复代码,这里虽然表面看起来只是减少重复代码,但是在这个基础上,可以延伸到维护、扩展层面的意义。假如我们有5个类似这样的不同业务或者维度的统计方法接口,有一天我们对最后数据处理阶段的逻辑有调整或者发现bug,那你就得重复改5个地方,但如果我们封装了一个方法来统一处理,那么改动就只有1个地方,这其实和软件架构中分层设计的思想、理念是一致的,只不过我们这里的粒度比较小,是在同一个类中提炼出了一个新的方法,但都是为了解决代码重复、耦合的问题。很多读者可能到目前为止依然对分层设计有种朦朦胧胧的感觉,那么通过本小节的实践,我们以封装一个方法来实现优化代码,以小见大,你是否对分层设计的理解又清晰了些呢?这里引用一句名言:

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决

在实现了服务层的代码之后,我们就可以来测试一下功能效果了,打开route.php,在analysis路由分组下新增一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        ............................
        // 统计分析相关接口
        Route::group('analysis', function () {
            // 时间范围统计订单数据
            Route::get('order/base', 'api/v1.Statistics/getOrderBaseStatistics');
            // 时间范围统计新增会员数
            Route::get('user/base', 'api/v1.Statistics/getUserBaseStatistics');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

路由定义完了之后打开Postman,按照路由信息新增并配置一个请求:

点击发送:

[
    {
        "date": "01",
        "count": 1
    },
    {
        "date": "02",
        "count": 1
    },
    {
        "date": "03",
        "count": 1
    },
    {
        "date": "04",
        "count": 1
    },
    {
        "date": "05",
        "count": 0
    }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

没有报错,这里同样实现了我们想要的效果,读者可自行根据自己数据库中user表的情况调整请求参数来测试效果。

最后更新: 2021-08-12 13:31:59
0/140
评论
0
暂无评论
  • 上一页
  • 首页
  • 1
  • 尾页
  • 下一页
  • 总共1页